package org.gdstash.file;

import net.jpountz.lz4.LZ4Factory;
import net.jpountz.lz4.LZ4FastDecompressor;
import org.gdstash.db.DBBitmap;
import org.gdstash.db.DBItem;
import org.gdstash.db.DBSkill;
import org.gdstash.db.DBSkillConnector;
import org.gdstash.util.GDConstants;
import org.gdstash.util.GDLog;
import org.gdstash.util.GDMsgFormatter;
import org.gdstash.util.GDMsgLogger;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.LinkedList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

public class ARCDecompress {
    private String fileName;
    private FileModule module;
    private FileType fileType;
    private ARCHeader header;
    private ARCFilePart[] parts;
    private ARCFileString[] strings;
    private ARCFileToC[] tocs;

    public ARCDecompress(final FileModule module, final String fileName) {
        this.module = module;
        this.fileName = fileName;
        final String extension = fileName.substring(fileName.length() - 4).toUpperCase(GDConstants.LOCALE_US);
        if (extension.equals(".ZIP")) {
            this.fileType = FileType.ZIP;
        } else {
            this.fileType = FileType.ARC;
        }
    }

    @Override
    public boolean equals(final Object o) {
        if (o == null) {
            return false;
        }
        if (!o.getClass().equals(ARCDecompress.class)) {
            return false;
        }
        final ARCDecompress arc = (ARCDecompress) o;
        return this.fileName.equals(arc.fileName);
    }

    public FileModule getModule() {
        return this.module;
    }

    public String getFileName() {
        return this.fileName;
    }

    private ARCHeader getHeader(final GDBuffer buffer) throws IOException, GDParseException {
        final ARCHeader header = new ARCHeader();
        final long temp = 0L;
        header.unknown = buffer.getInt();
        header.version = buffer.getInt();
        header.numFiles = buffer.getInt();
        header.numRecords = buffer.getInt();
        header.sizeRecords = buffer.getUnsignedInt();
        header.sizeStrings = buffer.getUnsignedInt();
        header.offsetRecords = buffer.getUnsignedInt();
        if (header.version != 3) {
            throw new GDParseException(GDMsgFormatter.getString(GDMsgFormatter.rbMsg, "ERR_UNSUPPORTED_VERSION"), 0L);
        }
        return header;
    }

    private ARCFilePart[] getFileParts(final GDBuffer buffer) throws IOException {
        if (this.header.numRecords == 0) {
            return null;
        }
        final ARCFilePart[] parts = new ARCFilePart[this.header.numRecords];
        buffer.setPosition(this.header.offsetRecords);
        for (int i = 0; i < parts.length; ++i) {
            final ARCFilePart part = new ARCFilePart();
            part.offset = buffer.getInt();
            part.lenCompressed = buffer.getInt();
            part.lenDecomp = buffer.getInt();
            parts[i] = part;
        }
        return parts;
    }

    private ARCFileString[] getStrings(final GDBuffer buffer) throws IOException {
        final ARCFileString[] strings = new ARCFileString[this.header.numFiles];
        buffer.setPosition(this.header.offsetRecords + this.header.sizeRecords);
        for (int i = 0; i < strings.length; ++i) {
            final String s = buffer.getStringNT();
            strings[i] = new ARCFileString(s);
        }
        return strings;
    }

    private ARCFileToC[] getFileToCs(final GDBuffer buffer) throws IOException {
        final ARCFileToC[] tocs = new ARCFileToC[this.header.numFiles];
        buffer.setPosition(this.header.offsetRecords + this.header.sizeRecords + this.header.sizeStrings);
        for (int i = 0; i < tocs.length; ++i) {
            final ARCFileToC toc = new ARCFileToC();
            toc.type = buffer.getInt();
            toc.offset = buffer.getInt();
            toc.lenCompressed = buffer.getInt();
            toc.lenDecomp = buffer.getInt();
            toc.hash = buffer.getInt();
            toc.fileTime = buffer.getLong();
            toc.numParts = buffer.getInt();
            toc.index = buffer.getInt();
            toc.lenString = buffer.getInt();
            toc.offsetString = buffer.getInt();
            tocs[i] = toc;
        }
        return tocs;
    }

    private void close() {
        this.header = null;
        this.parts = null;
        this.strings = null;
        this.tocs = null;
    }

    public static BufferedImage getImage(final byte[] data) {
        if (data == null) {
            return null;
        }
        BufferedImage image = null;
        try {
            image = DDSLoader.getImage(data);
        } catch (Exception ex) {
            image = null;
        }
        return image;
    }

    private void listStrings() {
        String s = null;
        s = "-----" + this.fileName + "-----";
        System.out.println(s);
        s = "-----STRINGS-----";
        System.out.println(s);
        s = "Index,Filename";
        System.out.println(s);
        int i = 0;
        for (final ARCFileString str : this.strings) {
            s = Integer.toString(i) + "," + str.getFileName();
            System.out.println(s);
            ++i;
        }
        System.out.println();
    }

    private void listToCs() {
        String s = null;
        s = "-----TOCS-----";
        System.out.println(s);
        s = "Index,Parts,Length (Decomp),Part Index";
        System.out.println(s);
        int i = 0;
        for (final ARCFileToC toc : this.tocs) {
            s = Integer.toString(i) + "," + toc.numParts + "," + toc.lenDecomp + "," + toc.index;
            System.out.println(s);
            ++i;
        }
        System.out.println();
    }

    private void listStringToCs() {
        String s = null;
        s = "-----STRING to TOC-----";
        System.out.println(s);
        s = "Index,Filename,ToC Index";
        System.out.println(s);
        int i = 0;
        for (final ARCFileString str : this.strings) {
            s = Integer.toString(i) + "," + str.getFileName() + "," + str.getToCIndex();
            System.out.println(s);
            ++i;
        }
        System.out.println();
    }

    private void determineToCIndexes() {
        boolean skipToCs = false;
        int numUnused = 0;
        for (int i = 0; i < this.tocs.length; ++i) {
            if (this.tocs[i].numParts == 0) {
                ++numUnused;
            }
        }
        if (numUnused > 0) {
            for (int i = 0; i < numUnused; ++i) {
                final String s = this.strings[this.strings.length - i - 1].getFileName();
                final int pos1 = s.lastIndexOf("/");
                final int pos2 = s.lastIndexOf(".");
                if (pos2 != -1) {
                    if (pos1 <= pos2) {
                        skipToCs = true;
                        break;
                    }
                }
            }
        }
        if (skipToCs) {
            for (int i = 0; i < this.strings.length; ++i) {
                this.strings[i].setToCIndex(i);
            }
            return;
        }
        int tocIndex = -1;
        int fileIndex = -1;
        final int tocMax = this.tocs.length - 1;
        final int fileMax = this.strings.length - 1;
        while (tocIndex < tocMax && fileIndex < fileMax) {
            ++tocIndex;
            if (this.tocs[tocIndex].lenDecomp == 0 && this.tocs[tocIndex].numParts == 0) {
                continue;
            }
            ++fileIndex;
            this.strings[fileIndex].setToCIndex(tocIndex);
        }
    }

    private int getFileIndex(final String fileName) {
        int index = -1;
        for (int i = 0; i < this.strings.length; ++i) {
            if (this.strings[i] != null) {
                if (fileName.equalsIgnoreCase(this.strings[i].getFileName())) {
                    index = i;
                    break;
                }
            }
        }
        return index;
    }

    private void decompressIndex(final GDBuffer buffer, final LZ4FastDecompressor decomp, final int index) throws IOException {
        if (this.strings[index].getData() != null) {
            return;
        }
        final int tocIndex = this.strings[index].getToCIndex();
        if (tocIndex == -1) {
            return;
        }
        byte[] data = null;
        if (this.tocs[tocIndex].type == 1 && this.tocs[tocIndex].lenCompressed == this.tocs[tocIndex].lenDecomp) {
            data = buffer.getByteArray(this.tocs[tocIndex].offset, this.tocs[tocIndex].lenDecomp);
        } else {
            data = new byte[this.tocs[tocIndex].lenDecomp];
            int offDecomp = 0;
            for (int j = 0; j < this.tocs[tocIndex].numParts; ++j) {
                final ARCFilePart part = this.parts[this.tocs[tocIndex].index + j];
                if (part.lenCompressed == part.lenDecomp) {
                    final byte[] bComp = buffer.getByteArray(part.offset, part.lenCompressed);
                    for (int k = 0; k < bComp.length; ++k) {
                        data[k + offDecomp] = bComp[k];
                    }
                } else {
                    final byte[] bComp = buffer.getByteArray(part.offset, part.lenCompressed);
                    decomp.decompress(bComp, 0, data, offDecomp, part.lenDecomp);
                }
                offDecomp += part.lenDecomp;
            }
        }
        this.strings[index].setData(data);
    }

    private File getFile() throws IOException {
        final File file = new File(this.fileName);
        if (!file.exists()) {
            final Object[] args = {file.getCanonicalPath()};
            final String s = GDMsgFormatter.format(GDMsgFormatter.rbMsg, "ERR_FILE_NOT_FOUND", args);
            throw new FileNotFoundException(s);
        }
        if (!file.isFile()) {
            final Object[] args = {file.getCanonicalPath()};
            final String s = GDMsgFormatter.format(GDMsgFormatter.rbMsg, "ERR_FILE_NOT_FOUND", args);
            throw new FileNotFoundException(s);
        }
        if (!file.canRead()) {
            final Object[] args = {file.getCanonicalPath()};
            final String s = GDMsgFormatter.format(GDMsgFormatter.rbMsg, "ERR_FILE_CANNOT_READ", args);
            throw new IOException(s);
        }
        return file;
    }

    public void decompress(final GDLog log) {
        if (this.fileType == FileType.ARC) {
            this.decompressARC(log);
        }
        if (this.fileType == FileType.ZIP) {
            this.decompressZIP();
        }
    }

    public static String removeComments(final String s) {
        String newS = "";
        String rest = s;
        int pos = 0;
        while (pos != -1) {
            pos = rest.indexOf("/*");
            if (pos == -1) {
                newS += rest;
            } else {
                newS += rest.substring(0, pos);
                rest = rest.substring(pos + 2);
                pos = rest.indexOf("*/");
                if (pos == -1) {
                    newS += rest;
                } else {
                    rest = rest.substring(pos + 2);
                }
            }
        }
        return newS;
    }

    private void decompressARCBuffer(final GDBuffer buffer) throws IOException {
        this.readFileInfo(buffer, null);
        this.extractFiles(buffer);
        if (buffer != null) {
            buffer.close();
        }
    }

    private String decompressZipFile(final ZipFile file, final ZipEntry entry) throws IOException {
        String text = "";
        StringBuffer line = new StringBuffer(128);
        final InputStream is = new BufferedInputStream(file.getInputStream(entry));
        final InputStreamReader isr = new InputStreamReader(is, GDConstants.CHARSET_PROPERTIES);
        final BufferedReader reader = new BufferedReader(isr);
        final int len = (int) entry.getSize();
        final char[] chars = new char[len];
        reader.read(chars, 0, len);
        int i = 0;
        while (i < chars.length) {
            if (chars[i] == '\r' && chars[i + 1] == '\n') {
                text = text + line.toString() + GDConstants.LINE_SEPARATOR;
                line = new StringBuffer(128);
                i += 2;
            } else {
                line.append(chars[i]);
                ++i;
            }
        }
        if (line.length() > 0) {
            text = text + line.toString() + GDConstants.LINE_SEPARATOR;
        }
        return text;
    }

    private static String convertControlChars(final String s, final boolean insertColor) {
        boolean colortag = false;
        String newS = "";
        String rest = removeComments(s);
        String word = null;
        int len = 0;
        int posControl = rest.indexOf("^");
        int posNewLine = rest.indexOf("{^n}");
        int pos;
        int posBlank = pos = rest.indexOf(" ");
        if (pos == -1) {
            if (posControl == -1) {
                pos = posNewLine;
            } else {
                pos = posControl;
                if (posNewLine != -1 && pos > posNewLine) {
                    pos = posNewLine;
                }
            }
        } else {
            if (posControl != -1 && pos > posControl) {
                pos = posControl;
            }
            if (posNewLine != -1 && pos > posNewLine) {
                pos = posNewLine;
            }
        }
        while (pos != -1) {
            if (pos == posBlank) {
                word = rest.substring(0, pos);
                rest = rest.substring(pos + 1);
                len += word.length();
                newS += word;
                if (insertColor) {
                    if (len < 80) {
                        newS += " ";
                        ++len;
                    } else {
                        newS += "<br>";
                        len = 0;
                    }
                } else {
                    newS += " ";
                    ++len;
                }
            }
            if (pos == posControl) {
                word = rest.substring(0, pos);
                rest = rest.substring(pos + 1);
                len += word.length();
                newS += word;
                try {
                    final char c = Character.toUpperCase(rest.charAt(0));
                    rest = rest.substring(1);
                    if (insertColor) {
                        boolean isColor = false;
                        String htmltag = null;
                        switch (c) {
                            case 'A': {
                                htmltag = "<font color = \"#60BC9C\">";
                                isColor = true;
                                break;
                            }
                            case 'B': {
                                htmltag = "<font color = \"#3298B5\">";
                                isColor = true;
                                break;
                            }
                            case 'C': {
                                htmltag = "<font color = \"#00C6C6\">";
                                isColor = true;
                                break;
                            }
                            case 'D': {
                                htmltag = "<font color = \"#404040\">";
                                isColor = true;
                                break;
                            }
                            case 'F': {
                                htmltag = "<font color = \"#F76782\">";
                                isColor = true;
                                break;
                            }
                            case 'G': {
                                htmltag = "<font color = \"#0DBC47\">";
                                isColor = true;
                                break;
                            }
                            case 'I': {
                                htmltag = "<font color = \"#5A039A\">";
                                isColor = true;
                                break;
                            }
                            case 'K': {
                                htmltag = "<font color = \"#A8A163\">";
                                isColor = true;
                                break;
                            }
                            case 'L': {
                                htmltag = "<font color = \"#7CAD00\">";
                                isColor = true;
                                break;
                            }
                            case 'M': {
                                htmltag = "<font color = \"#800000\">";
                                isColor = true;
                                break;
                            }
                            case 'N': {
                                htmltag = "<br>";
                                isColor = false;
                                len = 0;
                                break;
                            }
                            case 'O': {
                                htmltag = "<font color = \"#D38E43\">";
                                isColor = true;
                                break;
                            }
                            case 'P': {
                                htmltag = "<font color = \"#A767C6\">";
                                isColor = true;
                                break;
                            }
                            case 'R': {
                                htmltag = "<font color = \"#FF2E05\">";
                                isColor = true;
                                break;
                            }
                            case 'S': {
                                htmltag = "<font color = \"#787878\">";
                                isColor = true;
                                break;
                            }
                            case 'T': {
                                htmltag = "<font color = \"#00C6A2\">";
                                isColor = true;
                                break;
                            }
                            case 'W': {
                                htmltag = "<font color = \"#9A9A9A\">";
                                isColor = true;
                                break;
                            }
                            case 'Y': {
                                htmltag = "<font color = \"#B78021\">";
                                isColor = true;
                                break;
                            }
                        }
                        if (isColor) {
                            if (colortag) {
                                newS += "</font>";
                            }
                            colortag = true;
                        }
                        if (htmltag != null) {
                            newS += htmltag;
                        }
                    }
                } catch (IndexOutOfBoundsException ex) {
                }
                if (insertColor && len > 80) {
                    newS += "<br>";
                    len = 0;
                }
            }
            if (pos == posNewLine) {
                word = rest.substring(0, pos);
                rest = rest.substring(pos + 4);
                len = 0;
                newS = newS + word + "<br>";
            }
            posControl = rest.indexOf("^");
            posNewLine = rest.indexOf("{^n}");
            posBlank = (pos = rest.indexOf(" "));
            if (pos == -1) {
                if (posControl == -1) {
                    pos = posNewLine;
                } else {
                    pos = posControl;
                    if (posNewLine == -1 || pos <= posNewLine) {
                        continue;
                    }
                    pos = posNewLine;
                }
            } else {
                if (posControl != -1 && pos > posControl) {
                    pos = posControl;
                }
                if (posNewLine == -1 || pos <= posNewLine) {
                    continue;
                }
                pos = posNewLine;
            }
        }
        newS += rest;
        if (colortag) {
            newS += "</font>";
        }
        return newS;
    }

    private void extractFiles(final GDBuffer buffer) {
        final LZ4Factory factory = LZ4Factory.fastestInstance();
        final LZ4FastDecompressor decomp = factory.fastDecompressor();
        for (int i = 0; i < this.strings.length; ++i) {
            try {
                this.decompressIndex(buffer, decomp, i);
            } catch (IOException ex) {
                this.strings[i].setData(null);
            }
        }
        this.header = null;
        this.parts = null;
        this.tocs = null;
    }

    private void readFileInfo(final GDBuffer buffer, final GDLog log) {
        if (this.fileType == FileType.ZIP) {
            this.decompressZIP();
            return;
        }
        final GDLog tLog = new GDLog();
        try {
            this.header = this.getHeader(buffer);
            this.parts = this.getFileParts(buffer);
            this.strings = this.getStrings(buffer);
            this.tocs = this.getFileToCs(buffer);
            this.determineToCIndexes();
        } catch (Exception | Error ex) {
            tLog.addError(ex);
            if (ex.getClass().equals(OutOfMemoryError.class)) {
                String msg = null;
                msg = GDMsgFormatter.getString(GDMsgFormatter.rbMsg, "INFO_USE_64BIT_JAVA");
                tLog.addInfo(msg);
                msg = GDMsgFormatter.getString(GDMsgFormatter.rbMsg, "INFO_USE_BATCH_FILE");
                tLog.addInfo(msg);
            }
        }
        if (tLog.containsErrors()) {
            final Object[] args = {this.fileName};
            final String msg = GDMsgFormatter.format(GDMsgFormatter.rbMsg, "ERR_FILE_EXTRACT_FAIL", args);
            tLog.addError(msg);
        }
        if (log == null) {
            GDMsgLogger.addLog(tLog);
        } else {
            log.addLog(tLog);
        }
    }

    private void writeFileData(final String dir, final String subDir, final GDBuffer buffer, final LZ4FastDecompressor decomp, final GDLog log) {
        for (int i = 0; i < this.strings.length; ++i) {
            String newName = dir + GDConstants.FILE_SEPARATOR + subDir + GDConstants.FILE_SEPARATOR + this.strings[i].getFileName();
            for (int pos = newName.indexOf("/"); pos != -1; pos = newName.indexOf("/")) {
                newName = newName.substring(0, pos) + GDConstants.FILE_SEPARATOR + newName.substring(pos + 1);
            }
            try {
                this.decompressIndex(buffer, decomp, i);
                GDWriter.write(newName, this.strings[i].getData());
                if (newName.endsWith(".tex")) {
                    final String pngName = newName.substring(0, newName.length() - 4) + ".png";
                    final BufferedImage image = getImage(this.strings[i].getData());
                    if (image != null) {
                        final File fImg = new File(pngName);
                        ImageIO.write(image, "PNG", fImg);
                    }
                }
                this.strings[i].setData(null);
            } catch (IOException ex) {
                final Object[] args = {ex.getMessage(), this.strings[i].getFileName()};
                final String msg = GDMsgFormatter.format(GDMsgFormatter.rbMsg, "ERR_FILE_WRITE_ERROR", args);
                log.addError(msg);
            }
        }
    }

    private byte[] getTexture(final GDBuffer buffer, final LZ4FastDecompressor decomp, final String filename) {
        String arcname = null;
        String id = null;
        int pos = filename.indexOf("/");
        if (pos != -1) {
            arcname = GDConstants.FILE_SEPARATOR + filename.substring(0, pos) + ".ARC";
            arcname = arcname.toUpperCase(GDConstants.LOCALE_US);
            id = filename.substring(pos + 1);
        } else {
            id = filename;
        }
        if (arcname != null) {
            final String fn = this.getFileName().toUpperCase(GDConstants.LOCALE_US);
            pos = fn.indexOf(arcname);
            if (pos == -1) {
                return null;
            }
        }
        final int index = this.getFileIndex(id);
        if (index == -1) {
            return null;
        }
        try {
            this.decompressIndex(buffer, decomp, index);
        } catch (IOException ex) {
            this.strings[index].setData(null);
        }
        return this.strings[index].getData();
    }

    private BufferedImage getImage(final GDBuffer buffer, final LZ4FastDecompressor decomp, final String filename) {
        final byte[] data = this.getTexture(buffer, decomp, filename);
        return getImage(data);
    }

    private void decompressARC(final GDLog log) {
        final GDLog tLog = new GDLog();
        try {
            final File file = this.getFile();
            final GDBuffer buffer = ARZDecompress.getFileBuffer(file);
            this.decompressARCBuffer(buffer);
        } catch (Exception | Error ex) {
            tLog.addError(ex);
            if (ex.getClass().equals(OutOfMemoryError.class)) {
                String msg = null;
                msg = GDMsgFormatter.getString(GDMsgFormatter.rbMsg, "INFO_USE_64BIT_JAVA");
                tLog.addInfo(msg);
                msg = GDMsgFormatter.getString(GDMsgFormatter.rbMsg, "INFO_USE_BATCH_FILE");
                tLog.addInfo(msg);
            }
        }
        if (tLog.containsErrors()) {
            final Object[] args = {this.fileName};
            final String msg = GDMsgFormatter.format(GDMsgFormatter.rbMsg, "ERR_FILE_EXTRACT_FAIL", args);
            tLog.addError(msg);
        }
        if (log == null) {
            GDMsgLogger.addLog(tLog);
        } else {
            log.addLog(tLog);
        }
    }

    private void decompressZIP() {
        try (final ZipFile file = new ZipFile(this.fileName)) {
            final Enumeration<? extends ZipEntry> files = file.entries();
            final List<String> fileNames = new LinkedList<String>();
            while (files.hasMoreElements()) {
                final ZipEntry entry = files.nextElement();
                final String s = entry.getName();
                if (s.contains("tags")) {
                    fileNames.add(s);
                }
            }
            if (fileNames.size() == 0) {
                return;
            }
            this.strings = new ARCFileString[fileNames.size()];
            int i = 0;
            for (final String search : fileNames) {
                ZipEntry entry2 = null;
                entry2 = file.getEntry(search);
                if (entry2 != null) {
                    (this.strings[i] = new ARCFileString(search)).setToCIndex(i);
                    final String text = this.decompressZipFile(file, entry2);
                    this.strings[i].setText(text);
                }
                ++i;
            }
        } catch (Exception | Error ex) {
            GDMsgLogger.addError(ex);
        }
    }

    private String getArchivePrefix() {
        String fn = this.getFileName().toLowerCase(GDConstants.LOCALE_US);
        int pos = 0;
        pos = fn.lastIndexOf(GDConstants.FILE_SEPARATOR);
        if (pos != -1) {
            fn = fn.substring(pos + 1);
        }
        pos = fn.lastIndexOf(".arc");
        if (pos != -1) {
            fn = fn.substring(0, pos) + "/";
        }
        return fn;
    }

    private void updateImages(final GDBuffer buffer) throws IOException {
        final LZ4Factory factory = LZ4Factory.fastestInstance();
        final LZ4FastDecompressor decomp = factory.fastDecompressor();
        this.readFileInfo(buffer, null);
        final String fn = this.getArchivePrefix();
        final List<DBBitmap.ImageInfo> bitmapInfos = DBBitmap.getImageInfos(fn);
        for (final DBBitmap.ImageInfo info : bitmapInfos) {
            if (info.bitmapID != null && info.bitmap == null) {
                info.bitmap = this.getTexture(buffer, decomp, info.bitmapID);
            }
        }
        DBBitmap.updateImageInfo(bitmapInfos);
        final List<DBItem.ImageInfo> itemInfos = DBItem.getImageInfos(fn);
        for (final DBItem.ImageInfo info2 : itemInfos) {
            if (info2.bitmapID != null && info2.bitmap == null) {
                info2.bitmap = this.getTexture(buffer, decomp, info2.bitmapID);
            }
            if (info2.shardBitmapID != null && info2.shardBitmap == null) {
                info2.shardBitmap = this.getTexture(buffer, decomp, info2.shardBitmapID);
            }
        }
        DBItem.updateImageInfo(itemInfos);
        final List<DBSkill.ImageInfo> skillInfos = DBSkill.getImageInfos(fn);
        for (final DBSkill.ImageInfo info3 : skillInfos) {
            if (info3.bitmapDownID != null && info3.bitmapDown == null) {
                info3.bitmapDown = this.getTexture(buffer, decomp, info3.bitmapDownID);
            }
            if (info3.bitmapUpID != null && info3.bitmapUp == null) {
                info3.bitmapUp = this.getTexture(buffer, decomp, info3.bitmapUpID);
            }
        }
        DBSkill.updateImageInfo(skillInfos);
        final List<DBSkillConnector.ImageInfo> connectionInfos = DBSkillConnector.getImageInfos(fn);
        for (final DBSkillConnector.ImageInfo info4 : connectionInfos) {
            if (info4.connectionOffID != null && info4.connectionOff == null) {
                info4.connectionOff = this.getTexture(buffer, decomp, info4.connectionOffID);
            }
            if (info4.connectionOnID != null && info4.connectionOn == null) {
                info4.connectionOn = this.getTexture(buffer, decomp, info4.connectionOnID);
            }
        }
        DBSkillConnector.updateImageInfo(connectionInfos);
        buffer.close();
        this.close();
    }

    public void writeFiles(final String dir, final String subDir) {
        if (dir == null) {
            return;
        }
        final LZ4Factory factory = LZ4Factory.fastestInstance();
        final LZ4FastDecompressor decomp = factory.fastDecompressor();
        final GDLog log = new GDLog();
        try {
            final File file = this.getFile();
            final GDBuffer buffer = ARZDecompress.getFileBuffer(file);
            this.readFileInfo(buffer, log);
            this.writeFileData(dir, subDir, buffer, decomp, log);
        } catch (Exception | Error ex) {
            log.addError(ex);
            if (ex.getClass().equals(OutOfMemoryError.class)) {
                String msg = null;
                msg = GDMsgFormatter.getString(GDMsgFormatter.rbMsg, "INFO_USE_64BIT_JAVA");
                log.addInfo(msg);
                msg = GDMsgFormatter.getString(GDMsgFormatter.rbMsg, "INFO_USE_BATCH_FILE");
                log.addInfo(msg);
            }
        }
        this.close();
        GDMsgLogger.addLog(log);
    }

    public void updateImageData(final GDLog log) {
        final GDLog tLog = new GDLog();
        try {
            final File file = this.getFile();
            final GDBuffer buffer = new GDRandomAccessBuffer(file);
            this.updateImages(buffer);
        } catch (Exception | Error ex) {
            tLog.addError(ex);
            if (ex.getClass().equals(OutOfMemoryError.class)) {
                String msg = null;
                msg = GDMsgFormatter.getString(GDMsgFormatter.rbMsg, "INFO_USE_64BIT_JAVA");
                tLog.addInfo(msg);
                msg = GDMsgFormatter.getString(GDMsgFormatter.rbMsg, "INFO_USE_BATCH_FILE");
                tLog.addInfo(msg);
            }
        }
        if (tLog.containsErrors()) {
            final Object[] args = {this.fileName};
            final String msg = GDMsgFormatter.format(GDMsgFormatter.rbMsg, "ERR_FILE_EXTRACT_FAIL", args);
            tLog.addError(msg);
        }
        if (log == null) {
            GDMsgLogger.addLog(tLog);
        } else {
            log.addLog(tLog);
        }
    }

    private void convertText(final int index) {
        if (this.strings[index].getText() == null) {
            if (this.strings[index].getData() == null) {
                return;
            }
            String text = "";
            final DynamicByteArray line = new DynamicByteArray();
            int j = 0;
            while (j < this.strings[index].getData().length) {
                if (this.strings[index].getData()[j] == 13 && this.strings[index].getData()[j + 1] == 10) {
                    text = text + line.toString() + GDConstants.LINE_SEPARATOR;
                    line.clear();
                    j += 2;
                } else {
                    line.append(this.strings[index].getData()[j]);
                    ++j;
                }
            }
            if (line.length() > 0) {
                text = text + line.toString() + GDConstants.LINE_SEPARATOR;
            }
            if (text != null) {
                final ByteBuffer buffer = StandardCharsets.UTF_8.encode(text);
                final String utf8 = text = StandardCharsets.UTF_8.decode(buffer).toString();
            }
            this.strings[index].setText(text);
            this.strings[index].setData(null);
        }
    }

    private String findTag(final String text, final String tag) {
        final int pos1 = text.indexOf(tag + "=");
        if (pos1 == -1) {
            return null;
        }
        String result = null;
        int pos2 = text.indexOf(10, pos1);
        final int pos3 = text.indexOf(0, pos1);
        if (pos3 != -1 && pos3 < pos2) {
            pos2 = pos3;
        }
        if (pos2 == -1) {
            pos2 = pos3;
        }
        if (pos2 == -1) {
            result = text.substring(pos1 + tag.length() + 1);
        } else if (text.charAt(pos2 - 1) == '\r') {
            result = text.substring(pos1 + tag.length() + 1, pos2 - 1);
        } else {
            result = text.substring(pos1 + tag.length() + 1, pos2);
        }
        if (result != null) {
            final ByteBuffer buffer = StandardCharsets.UTF_8.encode(result);
            final String utf8 = result = StandardCharsets.UTF_8.decode(buffer).toString();
        }
        return result;
    }

    public String getTag(final List<String> filenames, final String tag, final boolean insertColor) {
        if (filenames == null) {
            return null;
        }
        if (this.strings == null) {
            return null;
        }
        String result = null;
        for (final String fn : filenames) {
            for (int i = 0; i < this.strings.length; ++i) {
                if (this.strings[i].getFileName().contains(fn)) {
                    if (this.strings[i].getText() == null) {
                        this.convertText(i);
                    }
                    if (this.strings[i].getText() != null) {
                        result = this.findTag(this.strings[i].getText(), tag);
                        break;
                    }
                }
            }
            if (result != null) {
                break;
            }
        }
        if (result == null) {
            for (int j = 0; j < this.strings.length; ++j) {
                if (this.strings[j] != null) {
                    if (this.strings[j].getText() == null) {
                        this.convertText(j);
                    }
                    if (this.strings[j].getText() != null) {
                        result = this.findTag(this.strings[j].getText(), tag);
                        if (result != null) {
                            break;
                        }
                    }
                }
            }
        }
        if (result != null) {
            result = convertControlChars(result, insertColor);
        }
        if (result != null) {
            final ByteBuffer buffer = StandardCharsets.UTF_8.encode(result);
            final String utf8 = result = StandardCharsets.UTF_8.decode(buffer).toString();
        }
        return result;
    }

    public enum FileType {
        ARC,
        ZIP;
    }

    public enum FileModule {
        GD,
        AoM,
        FG,
        Mod,
        All;
    }
}
